/*
 * #%L
 * Alfresco Repository
 * %%
 * Copyright (C) 2005 - 2023 Alfresco Software Limited
 * %%
 * This file is part of the Alfresco software. 
 * If the software was purchased under a paid Alfresco license, the terms of 
 * the paid license agreement will prevail.  Otherwise, the software is 
 * provided under the following open source license terms:
 * 
 * Alfresco is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * Alfresco is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License
 * along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
 * #L%
 */
package org.alfresco.repo.bulkimport.impl;

import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.alfresco.repo.batch.BatchProcessWorkProvider;
import org.alfresco.repo.bulkimport.AnalysedDirectory;
import org.alfresco.repo.bulkimport.DirectoryAnalyser;
import org.alfresco.repo.bulkimport.ImportableItem;
import org.alfresco.service.cmr.repository.NodeRef;

/**
 * A filesystem walker that returns all files and directories in subsequent levels of a filesystem tree; it returns all directories and files in a given level, at which point it moves on to the next level and starts returning directories and files in that level.
 * 
 * @since 4.0
 *
 */
public class StripingFilesystemTracker extends AbstractFilesystemTracker
{
    private ImportableItem rootFolder;
    private int currentLevel = 0;
    private int batchSize;

    // TODO choose most appropriate list type
    private Map<Integer, List<ImportableItem>> directoriesToProcess = new HashMap<Integer, List<ImportableItem>>(10);
    private List<ImportableItem> toProcess = new ArrayList<ImportableItem>();

    public StripingFilesystemTracker(DirectoryAnalyser directoryAnalyser, NodeRef target, File sourceFolder, int batchSize)
    {
        this.directoryAnalyser = directoryAnalyser;
        this.batchSize = batchSize;

        // not really an importable item but the interface requires it to be in this form
        rootFolder = new ImportableItem();
        rootFolder.getHeadRevision().setContentFile(sourceFolder.toPath());
        rootFolder.setNodeRef(target);

        addDirectoryToProcess(rootFolder, currentLevel);
    }

    protected void addDirectoriesToProcess(Collection<ImportableItem> dirsToAdd, int level)
    {
        List<ImportableItem> dirs = getDirectoriesToProcess(level);
        dirs.addAll(dirsToAdd);
    }

    protected void addDirectoryToProcess(ImportableItem dir, int level)
    {
        List<ImportableItem> dirs = getDirectoriesToProcess(level);
        dirs.add(dir);
    }

    protected List<ImportableItem> getDirectoriesToProcess(int level)
    {
        List<ImportableItem> dirs = directoriesToProcess.get(Integer.valueOf(level));
        if (dirs == null)
        {
            dirs = new ArrayList<ImportableItem>();
            directoriesToProcess.put(Integer.valueOf(level), dirs);
        }

        return dirs;
    }

    public int count()
    {
        // Note: this is an estimate of the number of directories and files in the current level
        // TODO guess - multiplier of number of directories to process in the current directory
        return numDirectoriesToProcess() * 100;
    }

    protected void incrementLevel()
    {
        currentLevel++;
    }

    public void itemImported(NodeRef nodeRef, ImportableItem importableItem)
    {
        // nothing to do
    }

    protected void addItemsToProcess(Collection<ImportableItem> items)
    {
        toProcess.addAll(items);
    }

    protected ImportableItem getDirectoryToProcess()
    {
        List<ImportableItem> dirs = getDirectoriesToProcess(currentLevel);
        if (dirs.size() > 0)
        {
            return dirs.remove(0);
        }
        else
        {
            return null;
        }
    }

    public boolean moreLevels()
    {
        return getDirectoriesToProcess(currentLevel).size() > 0;
    }

    public int numDirectoriesToProcess()
    {
        return getDirectoriesToProcess(currentLevel).size();
    }

    protected List<ImportableItem> getImportableItems(int count)
    {
        while (toProcess.size() < count)
        {
            ImportableItem directory = getDirectoryToProcess();
            if (directory != null)
            {
                AnalysedDirectory analysedDirectory = getImportableItemsInDirectory(directory);
                addItemsToProcess(analysedDirectory.getImportableDirectories());
                addItemsToProcess(analysedDirectory.getImportableItems());

                // add new directories to process in next level
                getDirectoriesToProcess(currentLevel + 1).addAll(analysedDirectory.getImportableDirectories());
            }
            else
            {
                break;
            }
        }

        int size = (toProcess.size() >= count ? count : toProcess.size());
        List<ImportableItem> result = new ArrayList<ImportableItem>(size);
        int i = size;
        while (i > 0)
        {
            // we can assume that there are items in toProcess to remove because the size has been pre-calculated above
            ImportableItem importableItem = toProcess.remove(0);
            if (importableItem != null)
            {
                result.add(importableItem);
                i--;
            }
            else
            {
                logger.warn("Unexpected empty toProcess queue");
            }
        }

        if (result.size() == 0)
        {
            // this level has been exhausted, increment level
            incrementLevel();
        }

        return result;
    }

    @Override
    public BatchProcessWorkProvider<ImportableItem> getWorkProvider()
    {
        BatchProcessWorkProvider<ImportableItem> provider = new BatchProcessWorkProvider<ImportableItem>() {
            @Override
            public int getTotalEstimatedWorkSize()
            {
                return count();
            }

            @Override
            public long getTotalEstimatedWorkSizeLong()
            {
                return count();
            }

            @Override
            public Collection<ImportableItem> getNextWork()
            {
                // TODO perhaps some multiple of the batchSize to limit calls
                // to getNextWork? Add this to repository.properties.
                return getImportableItems(batchSize * 1000);
            }
        };

        return provider;
    }
}
